<?php
/**
 * GAPI - Google Analytics PHP Interface
 * 
 * http://code.google.com/p/gapi-google-analytics-php-interface/
 * 
 * @copyright Stig Manning 2009
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @author Stig Manning <stig@sdm.co.nz>
 * @version 1.3
 * 
 */

class gapi
{
	const http_interface = 'auto'; //'auto': autodetect, 'curl' or 'fopen'
	

	const client_login_url = 'https://www.google.com/accounts/ClientLogin';
	const account_data_url = 'https://www.google.com/analytics/feeds/accounts/default';
	const report_data_url = 'https://www.google.com/analytics/feeds/data';
	const interface_name = 'GAPI-1.3';
	const dev_mode = false;
	
	private $auth_token = null;
	private $account_entries = array();
	private $account_root_parameters = array();
	private $report_aggregate_metrics = array();
	private $report_root_parameters = array();
	private $results = array();
	
	/**
	 * Constructor function for all new gapi instances
	 * 
	 * Set up authenticate with Google and get auth_token
	 *
	 * @param String $email
	 * @param String $password
	 * @param String $token
	 * @return gapi
	 */
	public function __construct($email, $password, $token = null)
	{
		if ($token !== null)
		{
			$this->auth_token = $token;
		}
		else
		{
			$this->authenticateUser($email, $password);
		}
	}
	
	/**
	 * Return the auth token, used for storing the auth token in the user session
	 *
	 * @return String
	 */
	public function getAuthToken()
	{
		return $this->auth_token;
	}
	
	/**
	 * Request account data from Google Analytics
	 *
	 * @param Int $start_index OPTIONAL: Start index of results
	 * @param Int $max_results OPTIONAL: Max results returned
	 */
	public function requestAccountData($start_index = 1, $max_results = 20)
	{
		$response = $this->httpRequest(gapi::account_data_url, array(
				'start-index' => $start_index, 
				'max-results' => $max_results), null, $this->generateAuthHeader());
		
		if (substr($response['code'], 0, 1) == '2')
		{
			return $this->accountObjectMapper($response['body']);
		}
		else
		{
			throw new Exception('GAPI: Failed to request account data. Error: "' . strip_tags($response['body']) . '"');
		}
	}
	
	/**
	 * Request report data from Google Analytics
	 *
	 * $report_id is the Google report ID for the selected account
	 * 
	 * $parameters should be in key => value format
	 * 
	 * @param String $report_id
	 * @param Array $dimensions Google Analytics dimensions e.g. array('browser')
	 * @param Array $metrics Google Analytics metrics e.g. array('pageviews')
	 * @param Array $sort_metric OPTIONAL: Dimension or dimensions to sort by e.g.('-visits')
	 * @param String $filter OPTIONAL: Filter logic for filtering results
	 * @param String $start_date OPTIONAL: Start of reporting period
	 * @param String $end_date OPTIONAL: End of reporting period
	 * @param Int $start_index OPTIONAL: Start index of results
	 * @param Int $max_results OPTIONAL: Max results returned
	 */
	public function requestReportData($report_id, $dimensions, $metrics, $sort_metric = null, $filter = null, $start_date = null, $end_date = null, $start_index = 1, $max_results = 30)
	{
		$parameters = array(
				'ids' => 'ga:' . $report_id);
		
		if (is_array($dimensions))
		{
			$dimensions_string = '';
			foreach ( $dimensions as $dimesion )
			{
				$dimensions_string .= ',ga:' . $dimesion;
			}
			$parameters['dimensions'] = substr($dimensions_string, 1);
		}
		else
		{
			$parameters['dimensions'] = 'ga:' . $dimensions;
		}
		
		if (is_array($metrics))
		{
			$metrics_string = '';
			foreach ( $metrics as $metric )
			{
				$metrics_string .= ',ga:' . $metric;
			}
			$parameters['metrics'] = substr($metrics_string, 1);
		}
		else
		{
			$parameters['metrics'] = 'ga:' . $metrics;
		}
		
		if ($sort_metric == null && isset($parameters['metrics']))
		{
			$parameters['sort'] = $parameters['metrics'];
		}
		elseif (is_array($sort_metric))
		{
			$sort_metric_string = '';
			
			foreach ( $sort_metric as $sort_metric_value )
			{
				//Reverse sort - Thanks Nick Sullivan
				if (substr($sort_metric_value, 0, 1) == "-")
				{
					$sort_metric_string .= ',-ga:' . substr($sort_metric_value, 1); // Descending
				}
				else
				{
					$sort_metric_string .= ',ga:' . $sort_metric_value; // Ascending
				}
			}
			
			$parameters['sort'] = substr($sort_metric_string, 1);
		}
		else
		{
			if (substr($sort_metric, 0, 1) == "-")
			{
				$parameters['sort'] = '-ga:' . substr($sort_metric, 1);
			}
			else
			{
				$parameters['sort'] = 'ga:' . $sort_metric;
			}
		}
		
		if ($filter != null)
		{
			$filter = $this->processFilter($filter);
			if ($filter !== false)
			{
				$parameters['filters'] = $filter;
			}
		}
		
		if ($start_date == null)
		{
			$start_date = date('Y-m-d', strtotime('1 month ago'));
		}
		
		$parameters['start-date'] = $start_date;
		
		if ($end_date == null)
		{
			$end_date = date('Y-m-d');
		}
		
		$parameters['end-date'] = $end_date;
		
		$parameters['start-index'] = $start_index;
		$parameters['max-results'] = $max_results;
		
		//$parameters['prettyprint'] = gapi::dev_mode ? 'true' : 'false';
		$parameters['prettyprint'] = false;
		
		$response = $this->httpRequest(gapi::report_data_url, $parameters, null, $this->generateAuthHeader());
		
		//HTTP 2xx
		if (substr($response['code'], 0, 1) == '2')
		{
			return $this->reportObjectMapper($response['body']);
		}
		else
		{
			throw new Exception('GAPI: Failed to request report data. Error: "' . strip_tags($response['body']) . '"');
		}
	}
	
	/**
	 * Process filter string, clean parameters and convert to Google Analytics
	 * compatible format
	 * 
	 * @param String $filter
	 * @return String Compatible filter string
	 */
	protected function processFilter($filter)
	{
		$valid_operators = '(!~|=~|==|!=|>|<|>=|<=|=@|!@)';
		
		$filter = preg_replace('/\s\s+/', ' ', trim($filter)); //Clean duplicate whitespace
		$filter = str_replace(array(
				',', 
				';'), array(
				'\,', 
				'\;'), $filter); //Escape Google Analytics reserved characters
		$filter = preg_replace('/(&&\s*|\|\|\s*|^)([a-z]+)(\s*' . $valid_operators . ')/i', '$1ga:$2$3', $filter); //Prefix ga: to metrics and dimensions
		$filter = preg_replace('/[\'\"]/i', '', $filter); //Clear invalid quote characters
		$filter = preg_replace(array(
				'/\s*&&\s*/', 
				'/\s*\|\|\s*/', 
				'/\s*' . $valid_operators . '\s*/'), array(
				';', 
				',', 
				'$1'), $filter); //Clean up operators
		

		if (strlen($filter) > 0)
		{
			return urlencode($filter);
		}
		else
		{
			return false;
		}
	}
	
	/**
	 * Report Account Mapper to convert the XML to array of useful PHP objects
	 *
	 * @param String $xml_string
	 * @return Array of gapiAccountEntry objects
	 */
	protected function accountObjectMapper($xml_string)
	{
		$xml = simplexml_load_string($xml_string);
		
		$this->results = null;
		
		$results = array();
		$account_root_parameters = array();
		
		//Load root parameters
		

		$account_root_parameters['updated'] = strval($xml->updated);
		$account_root_parameters['generator'] = strval($xml->generator);
		$account_root_parameters['generatorVersion'] = strval($xml->generator->attributes());
		
		$open_search_results = $xml->children('http://a9.com/-/spec/opensearchrss/1.0/');
		
		foreach ( $open_search_results as $key => $open_search_result )
		{
			$report_root_parameters[$key] = intval($open_search_result);
		}
		
		$account_root_parameters['startDate'] = strval($google_results->startDate);
		$account_root_parameters['endDate'] = strval($google_results->endDate);
		
		//Load result entries
		

		foreach ( $xml->entry as $entry )
		{
			$properties = array();
			foreach ( $entry->children('http://schemas.google.com/analytics/2009')->property as $property )
			{
				$properties[str_replace('ga:', '', $property->attributes()->name)] = strval($property->attributes()->value);
			}
			
			$properties['title'] = strval($entry->title);
			$properties['updated'] = strval($entry->updated);
			
			$results[] = new gapiAccountEntry($properties);
		}
		
		$this->account_root_parameters = $account_root_parameters;
		$this->results = $results;
		
		return $results;
	}
	
	/**
	 * Report Object Mapper to convert the XML to array of useful PHP objects
	 *
	 * @param String $xml_string
	 * @return Array of gapiReportEntry objects
	 */
	protected function reportObjectMapper($xml_string)
	{
		$xml = simplexml_load_string($xml_string);
		
		$this->results = null;
		$results = array();
		
		$report_root_parameters = array();
		$report_aggregate_metrics = array();
		
		//Load root parameters
		

		$report_root_parameters['updated'] = strval($xml->updated);
		$report_root_parameters['generator'] = strval($xml->generator);
		$report_root_parameters['generatorVersion'] = strval($xml->generator->attributes());
		
		$open_search_results = $xml->children('http://a9.com/-/spec/opensearchrss/1.0/');
		
		foreach ( $open_search_results as $key => $open_search_result )
		{
			$report_root_parameters[$key] = intval($open_search_result);
		}
		
		$google_results = $xml->children('http://schemas.google.com/analytics/2009');
		
		foreach ( $google_results->dataSource->property as $property_attributes )
		{
			$report_root_parameters[str_replace('ga:', '', $property_attributes->attributes()->name)] = strval($property_attributes->attributes()->value);
		}
		
		$report_root_parameters['startDate'] = strval($google_results->startDate);
		$report_root_parameters['endDate'] = strval($google_results->endDate);
		
		//Load result aggregate metrics
		

		foreach ( $google_results->aggregates->metric as $aggregate_metric )
		{
			$metric_value = strval($aggregate_metric->attributes()->value);
			
			//Check for float, or value with scientific notation
			if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value))
			{
				$report_aggregate_metrics[str_replace('ga:', '', $aggregate_metric->attributes()->name)] = floatval($metric_value);
			}
			else
			{
				$report_aggregate_metrics[str_replace('ga:', '', $aggregate_metric->attributes()->name)] = intval($metric_value);
			}
		}
		
		//Load result entries
		

		foreach ( $xml->entry as $entry )
		{
			$metrics = array();
			foreach ( $entry->children('http://schemas.google.com/analytics/2009')->metric as $metric )
			{
				$metric_value = strval($metric->attributes()->value);
				
				//Check for float, or value with scientific notation
				if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value))
				{
					$metrics[str_replace('ga:', '', $metric->attributes()->name)] = floatval($metric_value);
				}
				else
				{
					$metrics[str_replace('ga:', '', $metric->attributes()->name)] = intval($metric_value);
				}
			}
			
			$dimensions = array();
			foreach ( $entry->children('http://schemas.google.com/analytics/2009')->dimension as $dimension )
			{
				$dimensions[str_replace('ga:', '', $dimension->attributes()->name)] = strval($dimension->attributes()->value);
			}
			
			$results[] = new gapiReportEntry($metrics, $dimensions);
		}
		
		$this->report_root_parameters = $report_root_parameters;
		$this->report_aggregate_metrics = $report_aggregate_metrics;
		$this->results = $results;
		
		return $results;
	}
	
	/**
	 * Authenticate Google Account with Google
	 *
	 * @param String $email
	 * @param String $password
	 */
	protected function authenticateUser($email, $password)
	{
		$post_variables = array(
				'accountType' => 'GOOGLE', 
				'Email' => $email, 
				'Passwd' => $password, 
				'source' => gapi::interface_name, 
				'service' => 'analytics');
		
		$response = $this->httpRequest(gapi::client_login_url, null, $post_variables);

		//Convert newline delimited variables into url format then import to array
		parse_str(str_replace(array(
				"\n", 
				"\r\n"), '&', $response['body']), $auth_token);
		
		if (substr($response['code'], 0, 1) != '2' || ! is_array($auth_token) || empty($auth_token['Auth']))
		{
			echo 'GAPI: Failed to authenticate user. Error: "' . strip_tags($response['body']);
		}
		
		$this->auth_token = $auth_token['Auth'];
	}
	
	/**
	 * Generate authentication token header for all requests
	 *
	 * @return Array
	 */
	protected function generateAuthHeader()
	{
		return array(
				'Authorization: GoogleLogin auth=' . $this->auth_token);
	}
	
	/**
	 * Perform http request
	 * 
	 *
	 * @param Array $get_variables
	 * @param Array $post_variables
	 * @param Array $headers
	 */
	protected function httpRequest($url, $get_variables = null, $post_variables = null, $headers = null)
	{
		$interface = gapi::http_interface;
		
		if (gapi::http_interface == 'auto')
		{
			if (function_exists('curl_exec'))
			{
				$interface = 'curl';
			}
			else
			{
				$interface = 'fopen';
			}
		}
		
		if ($interface == 'curl')
		{
			return $this->curlRequest($url, $get_variables, $post_variables, $headers);
		}
		elseif ($interface == 'fopen')
		{
			return $this->fopenRequest($url, $get_variables, $post_variables, $headers);
		}
		else
		{
			throw new Exception('Invalid http interface defined. No such interface "' . gapi::http_interface . '"');
		}
	}
	
	/**
	 * HTTP request using PHP CURL functions
	 * Requires curl library installed and configured for PHP
	 * 
	 * @param Array $get_variables
	 * @param Array $post_variables
	 * @param Array $headers
	 */
	private function curlRequest($url, $get_variables = null, $post_variables = null, $headers = null)
	{
		$ch = curl_init();
		
		if (is_array($get_variables))
		{
			$get_variables = '?' . str_replace('&amp;', '&', urldecode(http_build_query($get_variables)));
		}
		else
		{
			$get_variables = null;
		}
		
		curl_setopt($ch, CURLOPT_URL, $url . $get_variables);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //CURL doesn't like google's cert
		

		if (is_array($post_variables))
		{
			curl_setopt($ch, CURLOPT_POST, true);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $post_variables);
		}
		
		if (is_array($headers))
		{
			curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		}
		
		$response = curl_exec($ch);
		$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		
		curl_close($ch);
		
		return array(
				'body' => $response, 
				'code' => $code);
	}
	
	/**
	 * HTTP request using native PHP fopen function
	 * Requires PHP openSSL
	 *
	 * @param Array $get_variables
	 * @param Array $post_variables
	 * @param Array $headers
	 */
	private function fopenRequest($url, $get_variables = null, $post_variables = null, $headers = null)
	{
		$http_options = array(
				'method' => 'GET', 
				'timeout' => 3);
		
		if (is_array($headers))
		{
			$headers = implode("\r\n", $headers) . "\r\n";
		}
		else
		{
			$headers = '';
		}
		
		if (is_array($get_variables))
		{
			$get_variables = '?' . str_replace('&amp;', '&', urldecode(http_build_query($get_variables)));
		}
		else
		{
			$get_variables = null;
		}
		
		if (is_array($post_variables))
		{
			$post_variables = str_replace('&amp;', '&', urldecode(http_build_query($post_variables)));
			$http_options['method'] = 'POST';
			$headers = "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($post_variables) . "\r\n" . $headers;
			$http_options['header'] = $headers;
			$http_options['content'] = $post_variables;
		}
		else
		{
			$post_variables = '';
			$http_options['header'] = $headers;
		}
		
		$context = stream_context_create(array(
				'http' => $http_options));
		$response = @file_get_contents($url . $get_variables, null, $context);
		
		return array(
				'body' => $response !== false ? $response : 'Request failed, fopen provides no further information', 
				'code' => $response !== false ? '200' : '400');
	}
	
	/**
	 * Case insensitive array_key_exists function, also returns
	 * matching key.
	 *
	 * @param String $key
	 * @param Array $search
	 * @return String Matching array key
	 */
	public static function array_key_exists_nc($key, $search)
	{
		if (array_key_exists($key, $search))
		{
			return $key;
		}
		if (! (is_string($key) && is_array($search)))
		{
			return false;
		}
		$key = strtolower($key);
		foreach ( $search as $k => $v )
		{
			if (strtolower($k) == $key)
			{
				return $k;
			}
		}
		return false;
	}
	
	/**
	 * Get Results
	 *
	 * @return Array
	 */
	public function getResults()
	{
		if (is_array($this->results))
		{
			return $this->results;
		}
		else
		{
			return;
		}
	}
	
	/**
	 * Get an array of the metrics and the matchning
	 * aggregate values for the current result
	 *
	 * @return Array
	 */
	public function getMetrics()
	{
		return $this->report_aggregate_metrics;
	}
	
	/**
	 * Call method to find a matching root parameter or 
	 * aggregate metric to return
	 *
	 * @param $name String name of function called
	 * @return String
	 * @throws Exception if not a valid parameter or aggregate 
	 * metric, or not a 'get' function
	 */
	public function __call($name, $parameters)
	{
		if (! preg_match('/^get/', $name))
		{
			throw new Exception('No such function "' . $name . '"');
		}
		
		$name = preg_replace('/^get/', '', $name);
		
		$parameter_key = gapi::array_key_exists_nc($name, $this->report_root_parameters);
		
		if ($parameter_key)
		{
			return $this->report_root_parameters[$parameter_key];
		}
		
		$aggregate_metric_key = gapi::array_key_exists_nc($name, $this->report_aggregate_metrics);
		
		if ($aggregate_metric_key)
		{
			return $this->report_aggregate_metrics[$aggregate_metric_key];
		}
		
		throw new Exception('No valid root parameter or aggregate metric called "' . $name . '"');
	}
}

/**
 * Class gapiAccountEntry
 * 
 * Storage for individual gapi account entries
 *
 */
class gapiAccountEntry
{
	private $properties = array();
	
	public function __construct($properties)
	{
		$this->properties = $properties;
	}
	
	/**
	 * toString function to return the name of the account
	 *
	 * @return String
	 */
	public function __toString()
	{
		if (isset($this->properties['title']))
		{
			return $this->properties['title'];
		}
		else
		{
			return;
		}
	}
	
	/**
	 * Get an associative array of the properties
	 * and the matching values for the current result
	 *
	 * @return Array
	 */
	public function getProperties()
	{
		return $this->properties;
	}
	
	/**
	 * Call method to find a matching parameter to return
	 *
	 * @param $name String name of function called
	 * @return String
	 * @throws Exception if not a valid parameter, or not a 'get' function
	 */
	public function __call($name, $parameters)
	{
		if (! preg_match('/^get/', $name))
		{
			throw new Exception('No such function "' . $name . '"');
		}
		
		$name = preg_replace('/^get/', '', $name);
		
		$property_key = gapi::array_key_exists_nc($name, $this->properties);
		
		if ($property_key)
		{
			return $this->properties[$property_key];
		}
		
		throw new Exception('No valid property called "' . $name . '"');
	}
}

/**
 * Class gapiReportEntry
 * 
 * Storage for individual gapi report entries
 *
 */
class gapiReportEntry
{
	private $metrics = array();
	private $dimensions = array();
	
	public function __construct($metrics, $dimesions)
	{
		$this->metrics = $metrics;
		$this->dimensions = $dimesions;
	}
	
	/**
	 * toString function to return the name of the result
	 * this is a concatented string of the dimesions chosen
	 * 
	 * For example:
	 * 'Firefox 3.0.10' from browser and browserVersion
	 *
	 * @return String
	 */
	public function __toString()
	{
		if (is_array($this->dimensions))
		{
			return implode(' ', $this->dimensions);
		}
		else
		{
			return '';
		}
	}
	
	/**
	 * Get an associative array of the dimesions
	 * and the matching values for the current result
	 *
	 * @return Array
	 */
	public function getDimensions()
	{
		return $this->dimensions;
	}
	
	/**
	 * Get an array of the metrics and the matchning
	 * values for the current result
	 *
	 * @return Array
	 */
	public function getMetrics()
	{
		return $this->metrics;
	}
	
	/**
	 * Call method to find a matching metric or dimension to return
	 *
	 * @param $name String name of function called
	 * @return String
	 * @throws Exception if not a valid metric or dimensions, or not a 'get' function
	 */
	public function __call($name, $parameters)
	{
		if (! preg_match('/^get/', $name))
		{
			throw new Exception('No such function "' . $name . '"');
		}
		
		$name = preg_replace('/^get/', '', $name);
		
		$metric_key = gapi::array_key_exists_nc($name, $this->metrics);
		
		if ($metric_key)
		{
			return $this->metrics[$metric_key];
		}
		
		$dimension_key = gapi::array_key_exists_nc($name, $this->dimensions);
		
		if ($dimension_key)
		{
			return $this->dimensions[$dimension_key];
		}
		
		throw new Exception('No valid metric or dimesion called "' . $name . '"');
	}
}